diff options
Diffstat (limited to 'apps/web/app/reader/[bookmarkId]/page.tsx')
| -rw-r--r-- | apps/web/app/reader/[bookmarkId]/page.tsx | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/apps/web/app/reader/[bookmarkId]/page.tsx b/apps/web/app/reader/[bookmarkId]/page.tsx new file mode 100644 index 00000000..7c2b0c9e --- /dev/null +++ b/apps/web/app/reader/[bookmarkId]/page.tsx @@ -0,0 +1,312 @@ +"use client"; + +import { Suspense, useState } from "react"; +import { useRouter } from "next/navigation"; +import HighlightCard from "@/components/dashboard/highlights/HighlightCard"; +import ReaderView from "@/components/dashboard/preview/ReaderView"; +import { Button } from "@/components/ui/button"; +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Slider } from "@/components/ui/slider"; +import { + HighlighterIcon as Highlight, + Minus, + Plus, + Printer, + Settings, + Type, + X, +} from "lucide-react"; + +import { api } from "@karakeep/shared-react/trpc"; +import { BookmarkTypes } from "@karakeep/shared/types/bookmarks"; +import { getBookmarkTitle } from "@karakeep/shared/utils/bookmarkUtils"; + +export default function ReaderViewPage({ + params, +}: { + params: { bookmarkId: string }; +}) { + const bookmarkId = params.bookmarkId; + const { data: highlights } = api.highlights.getForBookmark.useQuery({ + bookmarkId, + }); + const { data: bookmark } = api.bookmarks.getBookmark.useQuery({ + bookmarkId, + }); + + const router = useRouter(); + const [fontSize, setFontSize] = useState([18]); + const [lineHeight, setLineHeight] = useState([1.6]); + const [fontFamily, setFontFamily] = useState("serif"); + const [showHighlights, setShowHighlights] = useState(false); + const [showSettings, setShowSettings] = useState(false); + + const fontFamilies = { + serif: "ui-serif, Georgia, Cambria, serif", + sans: "ui-sans-serif, system-ui, sans-serif", + mono: "ui-monospace, Menlo, Monaco, monospace", + }; + + const onClose = () => { + if (window.history.length > 1) { + router.back(); + } else { + router.push("/dashboard"); + } + }; + + const handlePrint = () => { + window.print(); + }; + + return ( + <div className="min-h-screen bg-background"> + {/* Header */} + <header className="sticky top-0 z-40 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 print:hidden"> + <div className="flex h-14 items-center justify-between px-4"> + <div className="flex items-center gap-2"> + <Button variant="ghost" size="icon" onClick={onClose}> + <X className="h-4 w-4" /> + </Button> + <span className="text-sm text-muted-foreground">Reader View</span> + </div> + + <div className="flex items-center gap-2"> + <Button variant="ghost" size="icon" onClick={handlePrint}> + <Printer className="h-4 w-4" /> + </Button> + + <Popover open={showSettings} onOpenChange={setShowSettings}> + <PopoverTrigger asChild> + <Button variant="ghost" size="icon"> + <Settings className="h-4 w-4" /> + </Button> + </PopoverTrigger> + <PopoverContent side="bottom" align="end" className="w-80"> + <div className="space-y-4"> + <div className="flex items-center gap-2 pb-2"> + <Type className="h-4 w-4" /> + <h3 className="font-semibold">Reading Settings</h3> + </div> + + <div className="space-y-4"> + <div className="space-y-2"> + <label className="text-sm font-medium">Font Family</label> + <Select value={fontFamily} onValueChange={setFontFamily}> + <SelectTrigger> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="serif">Serif</SelectItem> + <SelectItem value="sans">Sans Serif</SelectItem> + <SelectItem value="mono">Monospace</SelectItem> + </SelectContent> + </Select> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium">Font Size</label> + <span className="text-sm text-muted-foreground"> + {fontSize[0]}px + </span> + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + setFontSize([Math.max(12, fontSize[0] - 1)]) + } + > + <Minus className="h-3 w-3" /> + </Button> + <Slider + value={fontSize} + onValueChange={setFontSize} + max={24} + min={12} + step={1} + className="flex-1" + /> + <Button + variant="outline" + size="icon" + className="h-7 w-7 bg-transparent" + onClick={() => + setFontSize([Math.min(24, fontSize[0] + 1)]) + } + > + <Plus className="h-3 w-3" /> + </Button> + </div> + </div> + + <div className="space-y-2"> + <div className="flex items-center justify-between"> + <label className="text-sm font-medium"> + Line Height + </label> + <span className="text-sm text-muted-foreground"> + {lineHeight[0]} + </span> + </div> + <Slider + value={lineHeight} + onValueChange={setLineHeight} + max={2.5} + min={1.2} + step={0.1} + /> + </div> + </div> + </div> + </PopoverContent> + </Popover> + + <Button + variant={showHighlights ? "default" : "ghost"} + size="icon" + onClick={() => setShowHighlights(!showHighlights)} + > + <Highlight className="h-4 w-4" /> + </Button> + </div> + </div> + </header> + + <div className="flex overflow-hidden"> + {/* Mobile backdrop */} + {showHighlights && ( + <button + className="fixed inset-0 top-14 z-40 bg-black/50 lg:hidden" + onClick={() => setShowHighlights(false)} + onKeyDown={(e) => { + if (e.key === "Escape") { + setShowHighlights(false); + } + }} + aria-label="Close highlights sidebar" + /> + )} + + {/* Main Content */} + <main + className={`flex-1 overflow-x-hidden transition-all duration-300 ${showHighlights ? "lg:mr-80" : ""}`} + > + <article className="mx-auto max-w-3xl overflow-x-hidden px-4 py-8 sm:px-6"> + {bookmark ? ( + <> + {/* Article Header */} + <header className="mb-8 space-y-4"> + <h1 + className="font-bold leading-tight" + style={{ + fontFamily: + fontFamilies[fontFamily as keyof typeof fontFamilies], + fontSize: `${fontSize[0] * 1.8}px`, + lineHeight: lineHeight[0] * 0.9, + }} + > + {getBookmarkTitle(bookmark)} + </h1> + <div className="flex items-center gap-4 text-sm text-muted-foreground"> + {bookmark.content.type == BookmarkTypes.LINK && ( + <span>By {bookmark.content.author}</span> + )} + <Separator orientation="vertical" className="h-4" /> + <span>8 min</span> + </div> + </header> + + {/* Article Content */} + <Suspense fallback={<FullPageSpinner />}> + <div className="overflow-x-hidden"> + <ReaderView + className="prose prose-neutral max-w-none break-words dark:prose-invert [&_code]:break-all [&_img]:h-auto [&_img]:max-w-full [&_pre]:overflow-x-auto [&_table]:block [&_table]:overflow-x-auto" + style={{ + fontFamily: + fontFamilies[fontFamily as keyof typeof fontFamilies], + fontSize: `${fontSize[0]}px`, + lineHeight: lineHeight[0], + }} + bookmarkId={bookmarkId} + /> + </div> + </Suspense> + </> + ) : ( + <FullPageSpinner /> + )} + </article> + </main> + + {/* Mobile backdrop */} + {showHighlights && ( + <button + className="fixed inset-0 top-14 z-40 bg-black/50 lg:hidden" + onClick={() => setShowHighlights(false)} + onKeyDown={(e) => { + if (e.key === "Escape") { + setShowHighlights(false); + } + }} + aria-label="Close highlights sidebar" + /> + )} + + {/* Highlights Sidebar */} + {showHighlights && highlights && ( + <aside className="fixed right-0 top-14 z-50 h-[calc(100vh-3.5rem)] w-full border-l bg-background sm:w-80 lg:z-auto lg:bg-background/95 lg:backdrop-blur lg:supports-[backdrop-filter]:bg-background/60 print:hidden"> + <div className="flex h-full flex-col"> + <div className="border-b p-4"> + <div className="flex items-center justify-between"> + <h2 className="font-semibold">Highlights</h2> + <div className="flex items-center gap-2"> + <span className="text-sm text-muted-foreground"> + {highlights.highlights.length} saved + </span> + <Button + variant="ghost" + size="icon" + className="h-6 w-6 lg:hidden" + onClick={() => setShowHighlights(false)} + > + <X className="h-4 w-4" /> + </Button> + </div> + </div> + </div> + + <div className="flex-1 overflow-auto p-4"> + <div className="space-y-4"> + {highlights.highlights.map((highlight) => ( + <HighlightCard + key={highlight.id} + highlight={highlight} + clickable={true} + /> + ))} + </div> + </div> + </div> + </aside> + )} + </div> + </div> + ); +} |
